﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Text;
using RayDen.Library.Core.Collections;
using RayDen.Library.Core.Primitives;

using RayDen.Library.Entity.Scene;

using Material = RayDen.Library.Data.Import.Loader3DS.MaterialEntity;
using Triangle = RayDen.Library.Data.Import.Loader3DS.ModelEntity.Triangle;
namespace RayDen.Library.Data.Import.Loader3DS
{

    public class SceneLoader : IGeometryLoader, IMaterialLoader
    {
        Dictionary<string, MaterialEntity> materials = new Dictionary<string, MaterialEntity>();
        private List<ModelEntity> models;
        private List<CameraEntity> cameras;
        //private List<LightEntity> lights;

        private void Load3ds(string file)
        {
            models = new List<ModelEntity>();
            cameras = new List<CameraEntity>();

            using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read))
            {
                using (var br = new BinaryReader(fs))
                {
                    var chunk = new ThreeDSChunk(br);
                    if (chunk.ID != ChunckIDs.MAIN3DS)
                    {
                        throw new ApplicationException("Not a 3ds file");
                    }
                    this.ProcessChunk(chunk, br);
                }
            }
            models = models.Where(item => item.vertices != null).ToList();
        }


        private void SkipChunk(ThreeDSChunk chunk, BinaryReader br)
        {
            int length = (int)chunk.Length - chunk.BytesRead;
            br.ReadBytes(length);
            chunk.BytesRead += length;
        }

        #region IGeometryLoader Members

        public SceneGeometryInfo Load(string fileName, bool invertNormals = false, params string[] additionalFiles)
        {
            this.Load3ds(fileName);

            var vertices = new List<Point>();
            int offset = 0;

            foreach (var model in this.models)
            {
                model.VertexOffset = offset;
                vertices.AddRange(model.vertices.Select(item => (Point)item));
                offset += model.vertices.Length;
            }

            var geo = models.Select(modelEntity => new GeometryInfo()
            {
                VertexData = new FloatStore(modelEntity.vertices.ToFloatList()),
                IndexData = new IntStore(modelEntity.indices.SelectMany(item => new[] {
                    (int)item.vertex1 + modelEntity.VertexOffset, 
                    (int)item.vertex2 + modelEntity.VertexOffset,
                    (int)item.vertex3 + modelEntity.VertexOffset
                })),
                NormalIndexData = new IntStore(),
                TextureIndexData = new IntStore(),
                MaterialName = modelEntity.material.MaterialName ?? "default",
                Name = modelEntity.ObjectName,
                NormalData = new FloatStore(modelEntity.normals.ToFloatList()),
                TextureData = new FloatStore(modelEntity.texcoords.ToFloatList()),
                GlobalIndexOffset = modelEntity.VertexOffset
            }).ToList();

            var geometry = new SceneGeometryInfo()
            {
                Vertices = vertices.ToArray(),
                TexCoords = this.models.SelectMany(item => item.texcoords ?? new UV[0]).ToVectorList().ToArray(),
                Normals = this.models.SelectMany(item => item.normals ?? new Vector[0]).ToArray(),
                Geometry = geo.ToArray(),
                Cameras = cameras.Select(item => new CameraInfo() { Position = (Point) item.Position, Direction = item.Direction, Up = new Vector(0, -1f, 0f), Fov = item.Lens }).ToArray(),
                GlobalIndexing = false
            };


            return geometry;
        }

        #endregion


        private void ProcessChunk(ThreeDSChunk chunk, BinaryReader reader)
        {
            while (chunk.BytesRead < chunk.Length)
            {
                var child = new ThreeDSChunk(reader);

                switch (child.ID)
                {
                    case 0x0002:

                        int version = reader.ReadInt32();
                        child.BytesRead += 4;

                        Console.WriteLine("3DS File Version: {0}", version);
                        break;

                    case ChunckIDs.EDIT3DS:
                        var obj_chunk = new ThreeDSChunk(reader);
                        // not sure whats up with this chunk
                        SkipChunk(obj_chunk, reader);
                        child.BytesRead += obj_chunk.BytesRead;
                        ProcessChunk(child, reader);
                        break;

                    case ChunckIDs.EDIT_MATERIAL:
                        ProcessMaterialChunk(child, reader);
                        //SkipChunk ( child );
                        break;
                    case ChunckIDs.EDIT_OBJECT:
                        //SkipChunk ( child );
                        string name = ProcessString(child, reader);
                        Console.WriteLine("OBJECT NAME: {0}", name);
                        var e = ProcessObjectChunk(child, null, reader);
                        e.ObjectName = name;
                        //e.CalculateNormals();
                        models.Add(e);
                        break;
                    default:
                        SkipChunk(child, reader);
                        break;

                }

                chunk.BytesRead += child.BytesRead;
                //Console.WriteLine ( "ID: {0} Length: {1} Read: {2}", chunk.ID.ToString("x"), chunk.Length , chunk.BytesRead );
            }
        }


        #region Chunk Handlers
        ModelEntity ProcessObjectChunk(ThreeDSChunk chunk, ModelEntity en, BinaryReader reader)
        {
            var e = en ?? new ModelEntity();
            while (chunk.BytesRead < chunk.Length)
            {
                var child = new ThreeDSChunk(reader);

                switch (child.ID)
                {
                    case ChunckIDs.OBJ_CAMERA:
                        this.ProcessCameraChunk(child, reader);
                        break;
                    case ChunckIDs.OBJ_TRIMESH:
                        ProcessObjectChunk(child, e, reader);
                        break;
                    case ChunckIDs.TRI_VERTEXL:
                        e.vertices = ReadVertices(child, reader);
                        break;
                    case ChunckIDs.TRI_FACEL1:
                        e.indices = ReadIndices(child, reader);

                        if (child.BytesRead < child.Length)
                            e = ProcessObjectChunk(child, e, reader);
                        break;
                    case ChunckIDs.TRI_OBJECT_MATERIAL:
                        string name2 = ProcessString(child, reader);
                        Console.WriteLine("	Uses Material: {0}", name2);

                        Material mat;
                        if (materials.TryGetValue(name2, out mat))
                            e.material = mat;
                        else
                            Console.WriteLine(" Warning: Material '{0}' not found. ", name2);
                        //throw new Exception ( "Material not found!" );

                        /*
                           int nfaces = reader.ReadUInt16 ();
                           child.BytesRead += 2;
                           Console.WriteLine ( nfaces );

                           for ( int ii=0; ii< nfaces+2; ii++)
                           {
                           Console.Write ( reader.ReadUInt16 () + " " );
                           child.BytesRead += 2;

                           }
                           */
                        SkipChunk(child, reader);
                        break;

                    case ChunckIDs.TRI_OBJECT_UV:
                        int cnt = reader.ReadUInt16();
                        child.BytesRead += 2;

                        Console.WriteLine("	TexCoords: {0}", cnt);
                        e.texcoords = new UV[cnt];
                        for (int ii = 0; ii < cnt; ii++)
                            e.texcoords[ii] = new UV(reader.ReadSingle(), reader.ReadSingle());

                        child.BytesRead += (cnt * (4 * 2));

                        break;

                    default:

                        SkipChunk(child, reader);
                        break;

                }
                chunk.BytesRead += child.BytesRead;
                //Console.WriteLine ( "	ID: {0} Length: {1} Read: {2}", chunk.ID.ToString("x"), chunk.Length , chunk.BytesRead );
            }
            return e;
        }

        private void ProcessCameraChunk(ThreeDSChunk chunk, BinaryReader reader)
        {
            var camera = new CameraEntity();
            camera.Position = new Vector(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
            camera.Direction = new Vector(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
            camera.Up = new Vector(0, 0, 1);

            float rotation = reader.ReadSingle();
            float lens = reader.ReadSingle();
            camera.Fov = 60f;
            camera.Rotation = rotation;
            camera.Lens = lens;
            chunk.BytesRead += (6 + 2) * 4;

            this.cameras.Add(camera);
            while (chunk.BytesRead < chunk.Length)
            {
                var child = new ThreeDSChunk(reader);
                switch (child.ID)
                {
                    case ChunckIDs.CAM_UNKNWN01:
                        SkipChunk(child, reader);
                        break;
                    default:
                        SkipChunk(child, reader);
                        break;
                }
                chunk.BytesRead += child.BytesRead;
            }
        }

        void ProcessMaterialChunk(ThreeDSChunk chunk, BinaryReader reader)
        {
            string name = string.Empty;
            var m = new Material();

            while (chunk.BytesRead < chunk.Length)
            {
                var child = new ThreeDSChunk(reader);

                switch (child.ID)
                {
                    case ChunckIDs.EDIT_MATNAME01:

                        name = ProcessString(child, reader);
                        m.MaterialName = name.ToLowerInvariant();
                        Console.WriteLine("Material: {0}", name);
                        break;

                    case ChunckIDs.EDIT_MATAMBIENT:

                        m.Ambient = ProcessColorChunk(child, reader);
                        break;

                    case ChunckIDs.EDIT_MATDIFFUSE:

                        m.Diffuse = ProcessColorChunk(child, reader);
                        break;

                    case ChunckIDs.EDIT_MATSPECULAR:

                        m.Specular = ProcessColorChunk(child, reader);
                        break;

                    case ChunckIDs.EDIT_MATSHININESS:

                        m.Shininess = ProcessPercentageChunk(child, reader);
                        //Console.WriteLine ( "SHININESS: {0}", m.Shininess );
                        break;
                    case ChunckIDs.EDIT_MATMAP:
                    case ChunckIDs.EDIT_MATMAP2:
                    case ChunckIDs.EDIT_MATMAPBUMP:
                    case ChunckIDs.EDIT_MATMAPREFLECTION:
                        ProcessPercentageChunk(child, reader);
                        //SkipChunk ( child );
                        ProcessTexMapChunk(child, m, reader);
                        break;
                    default:
                        SkipChunk(child, reader);
                        break;
                }
                chunk.BytesRead += child.BytesRead;
            }
            materials.Add(name, m);
        }



        float[] ProcessColorChunk(ThreeDSChunk chunk, BinaryReader reader)
        {
            ThreeDSChunk child = new ThreeDSChunk(reader);
            float[] c = new float[] { (float)reader.ReadByte() / 256, (float)reader.ReadByte() / 256, (float)reader.ReadByte() / 256 };
            //Console.WriteLine ( "R {0} G {1} B {2}", c.R, c.B, c.G );
            chunk.BytesRead += (int)child.Length;
            return c;
        }

        string ProcessString(ThreeDSChunk chunk, BinaryReader reader)
        {
            StringBuilder sb = new StringBuilder();

            byte b = reader.ReadByte();
            int idx = 0;
            while (b != 0)
            {
                sb.Append((char)b);
                b = reader.ReadByte();
                idx++;
            }
            chunk.BytesRead += idx + 1;

            return sb.ToString();
        }

        int ProcessPercentageChunk(ThreeDSChunk chunk, BinaryReader reader)
        {
            ThreeDSChunk child = new ThreeDSChunk(reader);
            int per = reader.ReadUInt16();
            child.BytesRead += 2;
            chunk.BytesRead += child.BytesRead;
            return per;
        }

        void ProcessTexMapChunk(ThreeDSChunk chunk, Material m, BinaryReader reader)
        {
            while (chunk.BytesRead < chunk.Length)
            {
                var child = new ThreeDSChunk(reader);
                switch ((Groups)child.ID)
                {
                    case Groups.C_MATMAPFILE:
                        string name = ProcessString(child, reader);
                        Console.WriteLine("	Texture File: {0}", name);
                        m.Textures.Add(name);
                        break;
                    default:
                        SkipChunk(child, reader);
                        break;

                }
                chunk.BytesRead += child.BytesRead;
            }
        }

        Vector[] ReadVertices(ThreeDSChunk chunk, BinaryReader reader)
        {
            ushort numVerts = reader.ReadUInt16();
            chunk.BytesRead += 2;
            Console.WriteLine("	Vertices: {0}", numVerts);
            Vector[] verts = new Vector[numVerts];

            for (int ii = 0; ii < verts.Length; ii++)
            {
                float f1 = reader.ReadSingle();
                float f2 = reader.ReadSingle();
                float f3 = reader.ReadSingle();

                verts[ii] = new Vector(f1, f3, -f2);
                //Console.WriteLine ( verts [ii] );
            }

            //Console.WriteLine ( "{0}   {1}", verts.Length * ( 3 * 4 ), chunk.Length - chunk.BytesRead );

            chunk.BytesRead += verts.Length * (3 * 4);
            //chunk.BytesRead = (int) chunk.Length;
            //SkipChunk ( chunk );

            return verts;
        }

        Triangle[] ReadIndices(ThreeDSChunk chunk, BinaryReader reader)
        {
            ushort numIdcs = reader.ReadUInt16();
            chunk.BytesRead += 2;
            Console.WriteLine("	Indices: {0}", numIdcs);
            Triangle[] idcs = new Triangle[numIdcs];

            for (int ii = 0; ii < idcs.Length; ii++)
            {
                idcs[ii] = new Triangle(reader.ReadUInt16(), reader.ReadUInt16(), reader.ReadUInt16());
                //Console.WriteLine ( idcs [ii] );

                // flags
                reader.ReadUInt16();
            }
            chunk.BytesRead += (2 * 4) * idcs.Length;
            //Console.WriteLine ( "b {0} l {1}", chunk.BytesRead, chunk.Length);

            //chunk.BytesRead = (int) chunk.Length;
            //SkipChunk ( chunk );

            return idcs;
        }
        #endregion

        #region IMaterialLoader Members

        public MaterialInfo[] LoadMaterials(string fileName, params string[] paths)
        {
            return this.materials.Select(
                mat =>
                {
                    var result = new MaterialInfo()
                    {
                        Name = mat.Key,
                        Ka = new RgbSpectrum(mat.Value.Ambient),
                        Kd = new RgbSpectrum(mat.Value.Diffuse),
                        Ks = new RgbSpectrum(mat.Value.Specular),
                        Exponent = mat.Value.Shininess
                    };
                    if (mat.Value.Textures.Any())
                    {
                        result.Diffuse = new ImageTextureInfo() { FilePath = mat.Value.Textures[0] };
                    }
                    return
                        result;
                }
                ).ToArray();
        }

        #endregion
    }
}
